﻿namespace Jsl.FxCop
{
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Linq;
    using Microsoft.FxCop.Sdk;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class Test_StatementFlow_without_branches
    {
        [TestMethod]
        public void StatementFlow_without_branches()
        {
            const string MethodCode = @"
public void Method()
{
    Mark(1);
    Mark(2);
}";
            new CompiledCode(MethodCode).Test(
                helper =>
                {
                    var x = helper.Flow.Statements;
                    var returnStatement = helper.Flow.Statements
                        .OfType<ReturnNode>()
                        .Single();

                    helper.SetStatement(helper.Marks[1]);
                    helper.PreviousStatements();
                    helper.FollowingStatements(helper.Marks[2]);

                    helper.SetStatement(helper.Marks[2]);
                    helper.PreviousStatements(helper.Marks[1]);
                    helper.FollowingStatements(returnStatement);

                    helper.SetStatement(returnStatement);
                    helper.PreviousStatements(helper.Marks[2]);
                    helper.FollowingStatements();
                });
        }

        [TestMethod]
        public void StatementFlow_with_simple_condition()
        {
            const string MethodCode = @"
public void Method()
{
    Mark(1);
    if (System.Console.Title == """")
    {
        Mark(2);
    }
    Mark(3);
}";

            new CompiledCode(MethodCode).Test(
                helper =>
                {
                    var assignmentStatement = helper.Debug ? helper.Flow.Statements.OfType<AssignmentStatement>().Single() : null;
                    var branchStatement = helper.Flow.Statements.OfType<Branch>().Single();
                    var returnStatement = helper.Flow.Statements.OfType<ReturnNode>().Single();

                    helper.SetStatement(helper.Marks[1]);
                    helper.PreviousStatements();
                    helper.FollowingStatements(helper.Debug ? (Statement)assignmentStatement : branchStatement);

                    if (helper.Debug)
                    {
                        helper.SetStatement(assignmentStatement);
                        helper.PreviousStatements(helper.Marks[1]);
                        helper.FollowingStatements(branchStatement);
                    }

                    helper.SetStatement(branchStatement);
                    helper.PreviousStatements(helper.Debug ? assignmentStatement : helper.Marks[1]);
                    helper.FollowingStatements(helper.Marks[2], helper.Marks[3]);

                    helper.SetStatement(helper.Marks[2]);
                    helper.PreviousStatements(branchStatement);
                    helper.FollowingStatements(helper.Marks[3]);

                    helper.SetStatement(helper.Marks[3]);
                    helper.PreviousStatements(branchStatement, helper.Marks[2]);
                    helper.FollowingStatements(returnStatement);

                    helper.SetStatement(returnStatement);
                    helper.PreviousStatements(helper.Marks[3]);
                    helper.FollowingStatements();
                });
        }

        [TestMethod]
        public void StatementFlow_with_multiple_ifs()
        {
            const string MethodCode = @"
public void Method()
{
    Mark(1);
    if (Test() == 1)
    {
        if (Test() == 2)
        {
            Mark(2);
        }
        Mark(3);
    }
    Mark(4);
}";

            new CompiledCode(MethodCode).Test(
                helper =>
                {
                    var assignmentStatement1 = helper.Debug ? helper.GetAssignmentStatement(1) : null;
                    var assignmentStatement2 = helper.Debug ? helper.GetAssignmentStatement(2) : null;
                    var branchToMarks3 = helper.GetBranchStatement(helper.Marks[3]);
                    var branchToMarks4 = helper.GetBranchStatement(helper.Marks[4]);

                    helper.SetStatement(helper.Marks[1]);
                    helper.PreviousStatements();
                    helper.DebugFollowingStatements(assignmentStatement1);
                    helper.ReleaseFollowingStatements(branchToMarks4);

                    if (helper.Debug)
                    {
                        helper.SetStatement(assignmentStatement1);
                        helper.PreviousStatements(helper.Marks[1]);
                        helper.FollowingStatements(branchToMarks4);
                    }

                    helper.SetStatement(branchToMarks4);
                    helper.DebugPreviousStatements(assignmentStatement1);
                    helper.ReleasePreviousStatements(helper.Marks[1]);
                    helper.DebugFollowingStatements(assignmentStatement2, helper.Marks[4]);
                    helper.ReleaseFollowingStatements(branchToMarks3, helper.Marks[4]);

                    if (helper.Debug)
                    {
                        helper.SetStatement(assignmentStatement2);
                        helper.PreviousStatements(branchToMarks4);
                        helper.FollowingStatements(branchToMarks3);
                    }

                    helper.SetStatement(branchToMarks3);
                    helper.FollowingStatements(helper.Marks[2], helper.Marks[3]);
                    helper.DebugPreviousStatements(assignmentStatement2);
                    helper.ReleasePreviousStatements(branchToMarks4);

                    helper.SetStatement(helper.Marks[2]);
                    helper.PreviousStatements(branchToMarks3);
                    helper.FollowingStatements(helper.Marks[3]);

                    helper.SetStatement(helper.Marks[3]);
                    helper.PreviousStatements(helper.Marks[2], branchToMarks3);
                    helper.FollowingStatements(helper.Marks[4]);

                    helper.SetStatement(helper.Marks[4]);
                    helper.PreviousStatements(helper.Marks[3], helper.GetBranchStatement(helper.Marks[4]));
                    helper.FollowingStatements(helper.Flow.Statements.OfType<ReturnNode>().First());

                    helper.SetStatement(helper.Flow.Statements.OfType<ReturnNode>().First());
                    helper.PreviousStatements(helper.Marks[4]);
                    helper.FollowingStatements();
                });
        }

        [TestMethod]
        public void StatementFlow_TryCatchFinally()
        {
            const string MethodCode = @"
public void Method()
{
    Mark(1);
    try
    {
        Mark(2);
    }
    catch (System.InvalidOperationException)
    {
        Mark(3);
    }
    catch
    {
        Mark(4);
    }
    finally
    {
        Mark(5);
    }

    Mark(6);
}";

            new CompiledCode(MethodCode).Test(
                helper =>
                {
                    var outerTryNode = helper.Flow.Method.Body.Statements
                        .OfType<TryNode>()
                        .Single();
                    var innerTryNode = outerTryNode.Block.Statements
                        .OfType<TryNode>()
                        .Single();
                    var finallyNode = helper.Flow.Statements
                        .OfType<FinallyNode>()
                        .Single();
                    var invalidOperationCatch = helper.Flow.Statements
                        .OfType<CatchNode>()
                        .Where(catchNode => catchNode.Type.Name.Name == "InvalidOperationException")
                        .First();
                    var objectCatch = helper.Flow.Statements
                        .OfType<CatchNode>()
                        .Where(catchNode => catchNode.Type.Name.Name == "Object")
                        .First();
                    var endFinallyNode = helper.Flow.Statements
                        .OfType<EndFinally>()
                        .First();
                    var returnNode = helper.Flow.Statements
                        .OfType<ReturnNode>()
                        .First();

                    helper.SetStatement(helper.Marks[1]);
                    helper.PreviousStatements();
                    helper.FollowingStatements(outerTryNode);

                    helper.SetStatement(outerTryNode);
                    helper.PreviousStatements(helper.Marks[1]);
                    helper.FollowingStatements(innerTryNode, finallyNode);

                    helper.SetStatement(innerTryNode);
                    helper.PreviousStatements(outerTryNode);
                    helper.FollowingStatements(helper.Marks[2], invalidOperationCatch, objectCatch);

                    helper.SetStatement(finallyNode);
                    helper.PreviousStatements(outerTryNode, helper.Marks[2], helper.Marks[3], helper.Marks[4]);
                    helper.FollowingStatements(helper.Marks[5]);

                    helper.SetStatement(invalidOperationCatch);
                    helper.PreviousStatements(innerTryNode);
                    helper.FollowingStatements(helper.Marks[3]);

                    helper.SetStatement(objectCatch);
                    helper.PreviousStatements(innerTryNode);
                    helper.FollowingStatements(helper.Marks[4]);

                    helper.SetStatement(helper.Marks[2]);
                    helper.PreviousStatements(innerTryNode);
                    helper.FollowingStatements(finallyNode);

                    helper.SetStatement(helper.Marks[3]);
                    helper.PreviousStatements(invalidOperationCatch);
                    helper.FollowingStatements(finallyNode);

                    helper.SetStatement(helper.Marks[4]);
                    helper.PreviousStatements(objectCatch);
                    helper.FollowingStatements(finallyNode);

                    helper.SetStatement(helper.Marks[5]);
                    helper.PreviousStatements(finallyNode);
                    helper.FollowingStatements(endFinallyNode);

                    helper.SetStatement(helper.Marks[6]);
                    helper.PreviousStatements(endFinallyNode);
                    helper.FollowingStatements(returnNode);

                    helper.SetStatement(returnNode);
                    helper.PreviousStatements(helper.Marks[6]);
                    helper.FollowingStatements();
                });
        }

        [TestMethod]
        public void Branch_to_try_is_fixed()
        {
            const string MethodCode = @"
protected void Method(bool disposing, object components)
{
    if (components != null)
    {
        Mark(1);
    }

    try
    {
        Mark(2);
    }
    finally
    {
        Mark(3);
    }
}";
            new CompiledCode(MethodCode).Test(
                helper =>
                {
                    var assignmentStatement = helper.Debug ? helper.Flow.Statements.OfType<AssignmentStatement>().First() : null;
                    var conditionalStatement = helper.Flow.Statements
                        .OfType<Branch>()
                        .Single();
                    var tryNode = helper.Flow.Statements
                        .OfType<TryNode>()
                        .Single();
                    var finallyNode = helper.Flow.Statements
                        .OfType<FinallyNode>()
                        .Single();
                    var endFinally = helper.Flow.Statements
                        .OfType<EndFinally>()
                        .Single();
                    var returnStatement = helper.Flow.Statements
                        .OfType<ReturnNode>()
                        .Single();

                    if (helper.Debug)
                    {
                        helper.SetStatement(assignmentStatement);
                        helper.PreviousStatements();
                        helper.FollowingStatements(conditionalStatement);
                    }

                    helper.SetStatement(helper.Marks[1]);
                    helper.PreviousStatements(conditionalStatement);
                    helper.FollowingStatements(tryNode);

                    helper.SetStatement(helper.Marks[2]);
                    helper.PreviousStatements(tryNode);
                    helper.FollowingStatements(finallyNode);

                    helper.SetStatement(finallyNode);
                    helper.PreviousStatements(tryNode, helper.Marks[2]);
                    helper.FollowingStatements(helper.Marks[3]);

                    helper.SetStatement(helper.Marks[3]);
                    helper.PreviousStatements(finallyNode);
                    helper.FollowingStatements(endFinally);

                    helper.SetStatement(endFinally);
                    helper.PreviousStatements(helper.Marks[3]);
                    helper.FollowingStatements(returnStatement);

                    helper.SetStatement(returnStatement);
                    helper.PreviousStatements(endFinally);
                    helper.FollowingStatements();
                });
        }
    }

    internal class CompiledCode
    {
        private readonly string code;
        private readonly Lazy<StatementFlow> lazyDebugStatementFlow;
        private readonly Lazy<StatementFlow> lazyReleaseStatementFlow;

        #region Constructors
        public CompiledCode(string code)
        {
            this.code = code;

            this.lazyReleaseStatementFlow = new Lazy<StatementFlow>(
                () => new StatementFlow(GetMethod(this.code, false)).Prune());
            this.lazyDebugStatementFlow = new Lazy<StatementFlow>(
                () => new StatementFlow(GetMethod(this.code, true)).Prune());
        }
        #endregion Constructors

        #region Public Poperties
        public StatementFlow DebugFlow
        {
            get { return this.lazyDebugStatementFlow.Value; }
        }

        public StatementFlow ReleaseFlow
        {
            get { return this.lazyReleaseStatementFlow.Value; }
        }
        #endregion Public Poperties

        #region Public Methods
        public static Method GetMethodFromClass(string classCode, string methodName, bool debugMode)
        {
            var assembly = new CompileHelper()
                .AddSource(classCode)
                .AddAssemblyReference("System.Windows.Forms.dll")
                .Generate(debugMode);
            return TypeNodeHelper.GetTypeNode(assembly, "Class")
                .GetMethod(method => method.Name.Name == methodName);
        }

        public static Method GetMethod(string methodCode, bool debugMode)
        {
            string fullCode =
@"public class Class
{
" + methodCode + @"

    private void Mark(int mark)
    {
    }

    private int Test()
    {
        return 999;
    }
}";
            return GetMethodFromClass(fullCode, "Method", debugMode);
        }

        public void Test(Action<FlowTestHelper> action)
        {
            this.TestDebug(action);
            this.TestRelease(action);
        }

        public void TestDebug(Action<FlowTestHelper> action)
        {
            action(new FlowTestHelper(this.DebugFlow, true));
        }

        public void TestRelease(Action<FlowTestHelper> action)
        {
            action(new FlowTestHelper(this.ReleaseFlow, false));
        }
        #endregion Public Methods
    }

    internal class FlowTestHelper
    {
        private readonly bool debug;
        private readonly StatementFlow flow;
        private readonly IDictionary<int, Statement> marks;
        private Statement statement;

        #region Constructors
        internal FlowTestHelper(StatementFlow flow, bool debug)
        {
            this.flow = flow;
            this.debug = debug;

            this.marks = GetMarks(flow);
        }
        #endregion Constructors

        #region Internal Properties
        internal bool Debug
        {
            get { return this.debug; }
        }

        internal StatementFlow Flow
        {
            get { return this.flow; }
        }

        internal IDictionary<int, Statement> Marks
        {
            get { return this.marks; }
        }
        #endregion Internal Properties

        #region Internal Methods
        internal AssignmentStatement GetAssignmentStatement(int value)
        {
            return flow.Statements
                .OfType<AssignmentStatement>()
                .Where(assignment => GetValueFromAssignment(assignment) == value)
                .First();
        }

        internal Branch GetBranchStatement(Statement statement)
        {
            return flow.Statements
                .OfType<Branch>()
                .Where(branch => branch.Target.Statements.Contains(statement))
                .First();
        }

        internal void SetStatement(Statement statement)
        {
            this.statement = statement;
        }

        internal void DebugFollowingStatements(params Statement[] expectedStatements)
        {
            if (this.Debug)
            {
                ElementsEqual(
                    this.flow.FollowingStatements(this.statement),
                    expectedStatements);
            }
        }

        internal void ReleaseFollowingStatements(params Statement[] expectedStatements)
        {
            if (!this.Debug)
            {
                ElementsEqual(
                    this.flow.FollowingStatements(this.statement),
                    expectedStatements);
            }
        }

        internal void FollowingStatements(params Statement[] expectedStatements)
        {
            ElementsEqual(
                this.flow.FollowingStatements(this.statement),
                expectedStatements);
        }

        internal void DebugPreviousStatements(params Statement[] expectedStatements)
        {
            if (this.Debug)
            {
                ElementsEqual(
                    this.flow.PreviousStatements(this.statement),
                    expectedStatements);
            }
        }

        internal void ReleasePreviousStatements(params Statement[] expectedStatements)
        {
            if (!this.Debug)
            {
                ElementsEqual(
                    this.flow.PreviousStatements(this.statement),
                    expectedStatements);
            }
        }

        internal void PreviousStatements(params Statement[] expectedStatements)
        {
            ElementsEqual(
                this.flow.PreviousStatements(this.statement),
                expectedStatements);
        }
        #endregion Internal Methods

        #region Private Methods
        private static void ElementsEqual(IEnumerable<Statement> actualStatements, params Statement[] expectedStatements)
        {
            var firstMissingExpectedStatement =
                (from expectedStatement in expectedStatements
                 where !actualStatements.Contains(expectedStatement)
                 select expectedStatement)
                .FirstOrDefault();
            if (firstMissingExpectedStatement != null)
            {
                Assert.Fail("Couldn't find the expected {0}.", firstMissingExpectedStatement.GetType());
            }

            var firstMissingActualStatement =
                (from actualStatement in actualStatements
                 where !expectedStatements.Contains(actualStatement)
                 select actualStatement)
                .FirstOrDefault();
            if (firstMissingActualStatement != null)
            {
                Assert.Fail("Found unexpected {0}.", firstMissingActualStatement.GetType());
            }
        }

        private static int GetMark(Statement statement)
        {
            ExpressionStatement expressionStatement = statement as ExpressionStatement;
            if (expressionStatement != null)
            {
                MethodCall call = expressionStatement.Expression as MethodCall;
                if (call != null)
                {
                    MemberBinding callee = call.Callee as MemberBinding;
                    if (callee != null)
                    {
                        if ((callee.TargetObject is This)
                            && (callee.BoundMember.FullName == "Class.Mark(" + WellKnownTypeNames.Int32 + ")"))
                        {
                            return (int)((Literal)call.Operands[0]).Value;
                        }
                    }
                }
            }

            return int.MinValue;
        }

        private static Dictionary<int, Statement> GetMarks(StatementFlow flow)
        {
            return flow.Statements
                .Where(statment => GetMark(statment) != int.MinValue)
                .ToDictionary(statement => GetMark(statement));
        }

        private static int GetValueFromBranch(Branch branch)
        {
            return GetValueFromTestExpression(branch.Condition);
        }

        private static int GetValueFromAssignment(AssignmentStatement assignment)
        {
            var outerBinary = (BinaryExpression)assignment.Source;
            var innerBinary = (BinaryExpression)outerBinary.Operand1;
            var literal = (Literal)innerBinary.Operand2;

            return (int)literal.Value;
        }

        private static int GetValueFromTestExpression(Expression expression)
        {
            var binaryExpression = expression as BinaryExpression;
            if (binaryExpression != null)
            {
                var literal = binaryExpression.Operand2 as Literal;
                if ((literal != null)
                    && (literal.Type.FullName == WellKnownTypeNames.Int32))
                {
                    return (int)literal.Value;
                }
            }

            throw new InvalidOperationException("Unable to find int constant.");
        }
        #endregion Private Methods
    }
}